<?php

/**
 * passwd generator
 * could using hash supported algorithm or php build in passwd generator
 *
 * @author  Appla<bhg@live.it>
 * @version  0.1
 * @date 2016-12-09
 */
class passwd
{
    /**
     * password_hash supported algorithm
     * @var array
     */
	const SUPPORTED_ALGORITHM = [PASSWORD_DEFAULT, PASSWORD_BCRYPT];

    /**
     *  password_hash supported option
     * @var array
     */
    const ALLOWED_PASSWD_HASH_OPTIONS = [
		'salt' => '',
		'cost' => '',
	];

    /**
     * password_hash algorithm
     * @var int
     */
	private static $passwdAlgo = PASSWORD_DEFAULT;

    /**
     * last generated password
     * @var string|null
     */
	private static $lastPasswd;

    /**
     * last generated salt
     * @var string|null
     */
	private static $lastSalt;

    /**
     * uniqid prefix
     * @var string
     */
	private static $uniqPrefix = 'kbPw';

    /**
     * hash support algo check cache
     * @var array
     */
    private static $isHashSupportedThisAlgorithm = [];

    /**
     * defaultPasswdSaltFormat
     * @var string
     */
    private static $defaultPasswdSaltFormat = '%s%s';

    /**
     * @param string $passwdStr
     * @param array $options
     * @return bool|string
     */
	public static function generate(string $passwdStr, array $options = []) : string
	{
		return $passwdStr ? static::$lastPasswd = password_hash($passwdStr, static::$passwdAlgo, array_intersect_key($options, static::ALLOWED_PASSWD_HASH_OPTIONS)): '';
	}

    /**
     * @param string $passwdHash
     * @return bool
     */
	public static function verify(string $passwdHash) : bool
	{
		return $passwdHash ? password_verify($passwdHash) : false;
	}

    /**
     * @param string $passwdHash
     * @return array
     */
	public static function hashInfo(string $passwdHash) : array
	{
		return $passwdHash ? password_get_info($passwdHash) : [];
	}

    /**
     * @param string $passwdHash
     * @param int $algo
     * @param array $options
     * @return bool
     */
	public static function isNeedRehashs(string $passwdHash, $algo, array $options) : bool
	{
		return $passwdHash ? password_needs_rehash($passwdHash, $algo, $options) : false;
	}

    /**
     * @param string $algo
     */
	public static function setAlgo(string $algo)
	{
		if(in_array($algo, static::$supportedPasswdAlgos)){
			static::$passwdAlgo = $algo;
		}
	}

    /**
     * @return null|string
     */
	public static function getLastSalt()
	{
		return static::$lastSalt;
	}

    /**
     * @return null|string
     */
	public static function getLastPasswd()
	{
		return static::$lastPasswd;
	}

    /**
     * @return string
     */
	public static function generateSalt() : string
	{
		return static::$lastSalt = hash('', uniqid(static::$uniqPrefix, true));
	}

    /**
     * @param string $passwdStr
     * @param string $salt
     * @return string
     */
	public static function md5Hash(string $passwdStr, $salt = '') : string
    {
        return static::hash(static::combineSalt($str, $salt), 'md5', false);
    }

    /**
     * @param string $passwdStr
     * @param string $hash
     * @return bool
     */
	public static function md5Verify(string $passwdStr, string $hash, $salt = '') : bool
	{
		return static::md5Hash($passwdStr, $salt) === $hash;
	}

    /**
     * @param string $str
     * @param string $algo
     * @param bool $raw
     * @return string
     */
	private static function hash(string $str, $algo = 'md5', $raw = false) : string
	{
		if(static::isHashSupported($algo)){
			return hash($algo, $str, $raw);
		}
		return '';
	}

    /**
     * hashVerify
     *
     * @param  string $algo
     * @param  string $passwdStr
     * @param  string $hash
     * @param  string $salt
     * @return boolean
     */
    private static function hashVerify(string $algo, string $passwdStr, string $hash, $salt = '') : bool
    {
        return static::hash(static::combineSalt($passwdStr, $salt), $algo, false) === $hash;
    }

    /**
     * check if hash support this algorithm
     * @param  string  $algo
     * @return boolean
     */
    private static function isHashSupported(string $algo) : bool
    {
        if(($algo = strtolower((string)$algo)) && !isset(static::$isHashSupportedThisAlgorithm[$algo])){
            static::$isHashSupportedThisAlgorithm[$algo] = in_array($algo, hash_algos());
        }
        return !empty(static::$isHashSupportedThisAlgorithm[$algo]);
    }

    /**
     * @param string $passwdStr
     * @param string $salt
     * @return string
     */
	public static function combineSalt(string $passwdStr, string $salt = '', string $tpl = '') : string
	{
        empty($tpl) && $tpl = static::$defaultPasswdSaltFormat;
		return sprintf($tpl, $passwdStr, $salt);
	}

    /**
     * __callStatic
     *
     * @param  string $method
     * @param  array $args
     * @return mixed
     */
    public static function __callStatic($method, $args)
    {
        $method = strtolower($method);
        if(substr($method, -4) === 'hash'){
            $algo = substr($method, 0, -4);
            if(static::isHashSupported($algo)){
                return call_user_func_array([__CLASS__, 'hash'], [call_user_func_array([__CLASS__, 'combineSalt'], array_slice($args, 0, 2)), $algo]);
            }
        }elseif(substr($method, -6) === 'verify' && count($args) > 1){
            $algo = substr($method, 0, -6);
            if(static::isHashSupported($algo)){
                array_unshift($args, $algo);
                return call_user_func_array([__CLASS__, 'hashVerify'], $args);
            }
        }
        throw new BadMethodCallException('method:'.$method.' NOT found in class');
    }
}
